package org.anita.movingpuzzle;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

public class MovingPuzzleBoardView extends View implements OnTouchListener,
		OnGestureListener {

	private static final int NO_ROTATION = 0;
	private static final int RIGTH_ROTATION = 1;
	private static final int UP_SIDE_DOWN_ROTATION = 2;
	private static final int LEFT_ROTATION = 3;

	private static final Matrix NO_ROTATION_MATRIX = new Matrix();
	private static final Matrix RIGHT_ROTATION_MATRIX = new Matrix();
	private static final Matrix UP_SIDE_DOWN_ROTATION_MATRIX = new Matrix();
	private static final Matrix LEFT_ROTATION_MATRIX = new Matrix();

	private static final Matrix[] ROTATIONS = { NO_ROTATION_MATRIX,
			RIGHT_ROTATION_MATRIX, UP_SIDE_DOWN_ROTATION_MATRIX,
			LEFT_ROTATION_MATRIX };

	static {
		NO_ROTATION_MATRIX.reset();
		RIGHT_ROTATION_MATRIX.reset();
		RIGHT_ROTATION_MATRIX.postRotate(90f);
		UP_SIDE_DOWN_ROTATION_MATRIX.reset();
		UP_SIDE_DOWN_ROTATION_MATRIX.postRotate(180f);
		LEFT_ROTATION_MATRIX.reset();
		LEFT_ROTATION_MATRIX.postRotate(270f);
	}

	private int widthTilesCount;
	private int heigthTilesCount;
	private Bitmap[] tileImages;
	private int[] tileRotation;
	private int[] tileOrdering;

	private int emptyTileIndex = -1;
	private int selectedTileIndex = -1;
	private int selectedTileXLocation = -1;
	private int selectedTileYLocation = -1;
	private int selectedTileXGrabLocation = -1;
	private int selectedTileYGrabLocation = -1;

	private GestureDetector gestureDetector;

	public MovingPuzzleBoardView(Context context) {
		super(context);

		init();
	}

	public MovingPuzzleBoardView(Context context, AttributeSet attrs) {
		super(context, attrs);

		init();
	}

	private void init() {
		setFocusable(true);
		setOnTouchListener(this);
		gestureDetector = new GestureDetector(getContext(), this);
	}

	@Override
	public Parcelable onSaveInstanceState() {

		Bundle bundle = new Bundle();
		bundle.putParcelable("instanceState", super.onSaveInstanceState());
		bundle.putIntArray("tileRotation", tileRotation);
		bundle.putIntArray("tileOrdering", tileOrdering);
		bundle.putInt("emptyTileIndex", emptyTileIndex);

		return bundle;
	}

	@Override
	public void onRestoreInstanceState(Parcelable state) {

		if (state instanceof Bundle) {
			Bundle bundle = (Bundle) state;
			this.tileRotation = bundle.getIntArray("tileRotation");
			this.tileOrdering = bundle.getIntArray("tileOrdering");
			this.emptyTileIndex = bundle.getInt("emptyTileIndex");
			super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
			return;
		}

		super.onRestoreInstanceState(state);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		for (int x = 0; x < widthTilesCount; ++x) {
			for (int y = 0; y < heigthTilesCount; ++y) {
				int tileIndex = y * widthTilesCount + x;
				int imageIndex = tileOrdering[tileIndex];
				if (tileIndex == selectedTileIndex) {
					continue;
				}
				Bitmap rotatedBitmap = Bitmap.createBitmap(
						tileImages[imageIndex], 0, 0,
						tileImages[imageIndex].getWidth(),
						tileImages[imageIndex].getHeight(),
						ROTATIONS[tileRotation[imageIndex]], false);
				canvas.drawBitmap(rotatedBitmap, null, new Rect(
						getXLocationForIndex(tileIndex),
						getYLocationForIndex(tileIndex),
						getXLocationForIndex(tileIndex) + getTileSize(),
						getYLocationForIndex(tileIndex) + getTileSize()), null);
			}
		}
		drawSelectedTile(canvas);
	}

	private void drawSelectedTile(Canvas canvas) {
		if (selectedTileIndex == -1) {
			return;
		}

		int imageIndex = tileOrdering[selectedTileIndex];
		Bitmap rotatedBitmap = Bitmap.createBitmap(tileImages[imageIndex], 0,
				0, tileImages[imageIndex].getWidth(),
				tileImages[imageIndex].getHeight(),
				ROTATIONS[tileRotation[selectedTileIndex]], false);
		canvas.drawBitmap(rotatedBitmap, null, new Rect(selectedTileXLocation,
				selectedTileYLocation, selectedTileXLocation + getTileSize(),
				selectedTileYLocation + getTileSize()), null);
	}

	public int getTileSize() {
		int width = getWidth() / widthTilesCount;
		int height = getHeight() / heigthTilesCount;
		return Math.min(width, height);
	}

	public int getXShift() {
		return (getWidth() - getTileSize() * widthTilesCount) / 2;
	}

	public int getYShift() {
		return (getHeight() - getTileSize() * heigthTilesCount) / 2;
	}

	public void initForSize(int widthTilesCount, int heigthTilesCount) {
		this.widthTilesCount = widthTilesCount;
		this.heigthTilesCount = heigthTilesCount;
		initBitmaps();
		initTileRotation();
		initTileOrdering();
	}

	public void setTileImage(int x, int y, Bitmap bitmap) {
		this.tileImages[x * widthTilesCount + y] = bitmap;
	}

	public void setTileImage(int x, int y, int resourceId) {
		this.tileImages[x * widthTilesCount + y] = BitmapFactory
				.decodeResource(getResources(), resourceId, null);
	}

	public void setEmptyTile(int x, int y) {
		emptyTileIndex = x * widthTilesCount + y;
		Bitmap defaultTile = createDefaultTile();
		tileImages[emptyTileIndex] = defaultTile;
	}

	private void initBitmaps() {
		this.tileImages = new Bitmap[widthTilesCount * heigthTilesCount];
		Bitmap defaultTile = createDefaultTile();
		for (int i = 0; i < widthTilesCount * heigthTilesCount; ++i) {
			tileImages[i] = defaultTile;
		}
	}

	protected Bitmap createDefaultTile() {
		return BitmapFactory.decodeResource(getResources(),
				R.drawable.default_tile, null);
	}

	private void initTileRotation() {
		this.tileRotation = new int[widthTilesCount * heigthTilesCount];
	}

	private void initTileOrdering() {
		this.tileOrdering = new int[widthTilesCount * heigthTilesCount];
		for (int i = 0; i < widthTilesCount * heigthTilesCount; ++i) {
			tileOrdering[i] = i;
		}
	}

	public boolean onTouch(View v, MotionEvent event) {
		int action = event.getAction();

		switch (action) {
		case MotionEvent.ACTION_DOWN: {
			grabTile(event.getX(), event.getY());
			gestureDetector.onTouchEvent(event);
			break;
		}
		case MotionEvent.ACTION_MOVE: {
			dragTile(event.getX(), event.getY());
			gestureDetector.onTouchEvent(event);
			break;
		}
		case MotionEvent.ACTION_UP: {
			dropTile(event.getX(), event.getY());
			gestureDetector.onTouchEvent(event);
			postInvalidate();
			break;
		}
		}

		return true;
	}

	private void grabTile(float x, float y) {
		int index = getCellIndex(x, y);
		if (isEmptyTileNeighbor(index)) {
			selectedTileIndex = index;
			selectedTileXLocation = getXLocationForIndex(selectedTileIndex);
			selectedTileYLocation = getYLocationForIndex(selectedTileIndex);
			selectedTileXGrabLocation = (int) x;
			selectedTileYGrabLocation = (int) y;
		}
	}

	private boolean isEmptyTileNeighbor(int index) {
		if (isEmptyTileRightNeighbor(index)) {
			return true;
		}

		if (isEmptyTileLeftNeighbor(index)) {
			return true;
		}

		if (isEmptyTileDownNeighbor(index)) {
			return true;
		}

		if (isEmptyTileTopNeighbor(index)) {
			return true;
		}

		return false;
	}

	private boolean isEmptyTileTopNeighbor(int index) {
		return index == emptyTileIndex + widthTilesCount;
	}

	private boolean isEmptyTileDownNeighbor(int index) {
		return index == emptyTileIndex - widthTilesCount;
	}

	private boolean isEmptyTileLeftNeighbor(int index) {
		return index == emptyTileIndex + 1;
	}

	private boolean isEmptyTileRightNeighbor(int index) {
		return index == emptyTileIndex - 1;
	}

	private void dragTile(float x, float y) {
		if (selectedTileIndex == -1) {
			return;
		}

		int xMove = (int) x - selectedTileXGrabLocation;
		int yMove = (int) y - selectedTileYGrabLocation;

		if (isEmptyTileLeftNeighbor(selectedTileIndex)) {
			selectedTileXLocation = selectedTileXLocation + xMove;
			enforceLocationConstraintsWhenMovingLeft();
		} else if (isEmptyTileRightNeighbor(selectedTileIndex)) {
			selectedTileXLocation = selectedTileXLocation + xMove;
			enforceLocationConstraintsWhenMovingRight();
		} else if (isEmptyTileTopNeighbor(selectedTileIndex)) {
			selectedTileYLocation = selectedTileYLocation + yMove;
			enforceLocationConstraintsWhenMovingUp();
		} else if (isEmptyTileDownNeighbor(selectedTileIndex)) {
			selectedTileYLocation = selectedTileYLocation + yMove;
			enforceLocationConstraintsWhenMovingDown();
		}

		selectedTileXGrabLocation = (int) x;
		selectedTileYGrabLocation = (int) y;
		postInvalidate();
	}

	private void enforceLocationConstraintsWhenMovingLeft() {
		if (selectedTileXLocation < getXLocationForIndex(emptyTileIndex)) {
			selectedTileXLocation = getXLocationForIndex(emptyTileIndex);
		}
		if (selectedTileXLocation > getXLocationForIndex(emptyTileIndex)
				+ getTileSize()) {
			selectedTileXLocation = getXLocationForIndex(emptyTileIndex)
					+ getTileSize();
		}
	}

	private void enforceLocationConstraintsWhenMovingRight() {
		if (selectedTileXLocation < getXLocationForIndex(selectedTileIndex)) {
			selectedTileXLocation = getXLocationForIndex(selectedTileIndex);
		}
		if (selectedTileXLocation > getXLocationForIndex(selectedTileIndex)
				+ getTileSize()) {
			selectedTileXLocation = getXLocationForIndex(selectedTileIndex)
					+ getTileSize();
		}
	}

	private void enforceLocationConstraintsWhenMovingUp() {
		if (selectedTileYLocation < getYLocationForIndex(emptyTileIndex)) {
			selectedTileYLocation = getYLocationForIndex(emptyTileIndex);
		}
		if (selectedTileYLocation > getYLocationForIndex(emptyTileIndex)
				+ getTileSize()) {
			selectedTileYLocation = getYLocationForIndex(emptyTileIndex)
					+ getTileSize();
		}
	}

	private void enforceLocationConstraintsWhenMovingDown() {
		if (selectedTileYLocation < getYLocationForIndex(selectedTileIndex)) {
			selectedTileYLocation = getYLocationForIndex(selectedTileIndex);
		}
		if (selectedTileYLocation > getYLocationForIndex(selectedTileIndex)
				+ getTileSize()) {
			selectedTileYLocation = getYLocationForIndex(selectedTileIndex)
					+ getTileSize();
		}
	}

	private int getXLocationForIndex(int index) {
		return (index % widthTilesCount) * getTileSize() + getXShift();
	}

	private int getYLocationForIndex(int index) {
		return (index / widthTilesCount) * getTileSize() + getYShift();
	}

	private void dropTile(float x, float y) {
		if (selectedTileIndex == -1) {
			return;
		}

		int currentLocationIndex = getCellIndex(x, y);
		if (currentLocationIndex == emptyTileIndex) {
			int selectedTileImagesIndex = tileOrdering[selectedTileIndex];
			tileOrdering[selectedTileIndex] = tileOrdering[emptyTileIndex];
			tileOrdering[emptyTileIndex] = selectedTileImagesIndex;
			emptyTileIndex = selectedTileIndex;
		}

		selectedTileIndex = -1;
		selectedTileXLocation = -1;
		selectedTileYLocation = -1;
		selectedTileXGrabLocation = -1;
		selectedTileYGrabLocation = -1;
	}

	private int getCellIndex(float x, float y) {
		int tileSize = getTileSize();

		int xIndex = (int) ((x - getXShift()) / tileSize);
		int yIndex = (int) ((y - getYShift()) / tileSize);

		// clamp selection to edges of puzzle
		if (xIndex >= widthTilesCount) {
			xIndex = widthTilesCount - 1;
		} else if (xIndex < 0) {
			xIndex = 0;
		}

		if (yIndex >= heigthTilesCount) {
			yIndex = heigthTilesCount - 1;
		} else if (yIndex < 0) {
			yIndex = 0;
		}

		return widthTilesCount * yIndex + xIndex;
	}

	public boolean onDown(MotionEvent e) {
		return true;
	}

	public void onShowPress(MotionEvent e) {
	}

	public boolean onSingleTapUp(MotionEvent e) {
		int index = getCellIndex(e.getX(), e.getY());
		int imageIndex = tileOrdering[index];
		tileRotation[imageIndex] = (tileRotation[imageIndex] + 1) % 4;
		return true;
	}

	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
			float distanceY) {
		return true;
	}

	public void onLongPress(MotionEvent e) {
	}

	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
			float velocityY) {
		return true;
	}

}
